Car Price Prediction - Exploratory data analysis. No description has been provided for this image


Вступление¶

EDA (разведочный анализ данных) - это процесс изучения основных свойств данных, нахождения в них закономерностей, аномалий и различных распределений, построений моделей с помощью визуализации.

Известно, что не существует единой последовательности операций для выполнения анализа, но есть базовые пункты, которые необходимо выполнить.

Анализ проводится над выбранном в kaggle dataset - см. ссылки далее.

Информация об dataset.¶

Dataset - Car Price Prediction Challenge - является набором данных об различных проданных автомобилях с их параметрами.
Например:

  1. Manufacturer (Производитель)
  2. Leather interior (Кожаный салон)
  3. Fuel type (Тип топлива)
    и т.д ...

Всего таких параметров - 15.

Ссылка на оригинальный dataset - https://www.kaggle.com/datasets/deepcontractor/car-price-prediction-challenge?resource=download
Ссылка на dataset, сохраненный в Google Drive - https://docs.google.com/spreadsheets/d/1PMhtD3LqyCzlZMEh-8aDPxre0wPw8v0U/edit?usp=drive_link&ouid=100105970921534140705&rtpof=true&sd=true

Цель (target) EDA.¶

Целью EDA является - выявление ключевых факторов (features), влияющих на стоимость.

Также передо мной стоит цель соответствовать следующим метрикам (как минимум первым двум):

img.png


Этапы EDA.¶

1) Загрузка данных с сайта и сохранения их в директории для дальнейшего взаимодействия¶

[!IMPORTANT]
Всегда активируем окружение как рассказано в файле README.md.
Не забываем добавить в нашего окружение (Conda+Poetry) необходимые зависимости:

  1. pandas
  2. numpy
  3. seaborn
  4. matplotlib
  5. openpyxl
  6. plotly

В первую очередь, так как датасет не сохранен в директории проекта, необходимо создать директорию data в корне. А после выполнить в созданную директорию загрузку сырых данных в формате xlsx.
Выполним подключение основных библиотек для проведения EDA.

In [1]:
import os
import requests
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.colors as mc
import plotly.express as px
import plotly.io as pio
import plotly.graph_objects as go
from dotenv import load_dotenv
pio.renderers.default = 'notebook'

%matplotlib inline

load_dotenv()

file_id = os.getenv("FILE_ID")

Выполним подключение кастомной палитры для графиков в EDA.

In [2]:
color = "#f1faee"  # проставляем цвет фона - АКТУАЛЬНЫЙ f1faee
palette = plt.cm.OrRd(
    np.linspace(0.5, 1.0, 12)
)  # настраиваем палитру OrRd, убирая самые светлые оттенки
translate_into_hex = [
    f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" for r, g, b, a in palette
]  # переводим палитру в HEX формат для применения в настройках Matplotlib, Seaborn, Plotly


plt.rcParams.update(
    {
        "font.family": "Arial",
        "font.size": 12,
        "figure.figsize": (14, 6),
        "figure.facecolor": f"{color}",
        "axes.facecolor": f"{color}",
        "grid.color": "#f0f0f0",
        "axes.prop_cycle": plt.cycler(color=translate_into_hex),
    }
)

sns.set_theme(
    style="whitegrid",
    palette=translate_into_hex,
    font="Arial",
    font_scale=1.0,
    rc={
        "figure.figsize": (14, 6),
        "figure.facecolor": f"{color}",
        "axes.facecolor": f"{color}",
        "grid.color": "#f0f0f0",
    },
)

custom_template = go.layout.Template()
custom_template.layout.colorway = translate_into_hex
custom_template.layout.font = {"family": "Arial", "size": 12}
custom_template.layout.title = {"x": 0.5, "font": {"size": 16, "weight": "bold"}}
custom_template.layout.paper_bgcolor = f"{color}"
custom_template.layout.plot_bgcolor = f"{color}"


pio.templates["my_custom"] = custom_template
pio.templates.default = "my_custom"

Здесь выполняем загрузку dataset с GD или чтение уже имеющегося.

In [3]:
file_url = f"https://drive.google.com/uc?id={file_id}"

pd.set_option("display.max_columns", 20)  # убирает ограничения отображения dataset
pd.set_option("display.max_rows", 150)

data_dir = "../data/EDA"
if not os.path.exists(data_dir):
    os.makedirs(data_dir)

data_path = os.path.join(data_dir, "data_car.xlsx")

if os.path.isfile(data_path):  # если файл есть в data - читаем, если нет - скачиваем
    df = pd.read_excel(data_path)
    df = df.replace("-", pd.NA)  # убираем все ложные пропуски на NaN
    print(f"{'-'*60}")
    print("Dataset прочитан из директории data...")
    print(f"{'-'*60}")
else:
    response = requests.get(file_url)
    f_path = os.path.join(data_dir, "data_car.xlsx")
    with open(f_path, "wb") as f:
        f.write(response.content)

    df = pd.read_excel(f_path)
    df = df.replace("-", pd.NA)
    print(f"{'-'*60}")
    print("Dataset загружен в директорию data и прочитан...")
    print(f"{'-'*60}")
------------------------------------------------------------
Dataset загружен в директорию data и прочитан...
------------------------------------------------------------

2) Первичный осмотр данных¶

После чтения/загрузки dataset из директории/в директорию необходимо осмотреть полученные данные и провести первичную оценку.
Поэтому выведем 10 первых и 10 последних значений dataset. А также проверим количество признаков и элементов.

In [4]:
df.head(10)
Out[4]:
ID Price Levy Manufacturer Model Prod. year Category Leather interior Fuel type Engine volume Mileage Cylinders Gear box type Drive wheels Doors Wheel Color Airbags
0 45654403 13328 1399 LEXUS RX 450 2010 Jeep Yes Hybrid 3.5 186005 km 6.0 Automatic 4x4 04-May Left wheel Silver 12
1 44731507 16621 1018 CHEVROLET Equinox 2011 Jeep No Petrol 3 192000 km 6.0 Tiptronic 4x4 04-May Left wheel Black 8
2 45774419 8467 <NA> HONDA FIT 2006 Hatchback No Petrol 1.3 200000 km 4.0 Variator Front 04-May Right-hand drive Black 2
3 45769185 3607 862 FORD Escape 2011 Jeep Yes Hybrid 2.5 168966 km 4.0 Automatic 4x4 04-May Left wheel White 0
4 45809263 11726 446 HONDA FIT 2014 Hatchback Yes Petrol 1.3 91901 km 4.0 Automatic Front 04-May Left wheel Silver 4
5 45802912 39493 891 HYUNDAI Santa FE 2016 Jeep Yes Diesel 2 160931 km 4.0 Automatic Front 04-May Left wheel White 4
6 45656768 1803 761 TOYOTA Prius 2010 Hatchback Yes Hybrid 1.8 258909 km 4.0 Automatic Front 04-May Left wheel White 12
7 45816158 549 751 HYUNDAI Sonata 2013 Sedan Yes Petrol 2.4 216118 km 4.0 Automatic Front 04-May Left wheel Grey 12
8 45641395 1098 394 TOYOTA Camry 2014 Sedan Yes Hybrid 2.5 398069 km 4.0 Automatic Front 04-May Left wheel Black 12
9 45756839 26657 <NA> LEXUS RX 350 2007 Jeep Yes Petrol 3.5 128500 km 6.0 Automatic 4x4 04-May Left wheel Silver 12
In [5]:
df.tail(10)
Out[5]:
ID Price Levy Manufacturer Model Prod. year Category Leather interior Fuel type Engine volume Mileage Cylinders Gear box type Drive wheels Doors Wheel Color Airbags
19227 45769427 29793 1053 MERCEDES-BENZ E 350 2014 Sedan Yes Diesel 3.5 219030 km 6.0 Automatic 4x4 04-May Left wheel Black 12
19228 45773726 706 1850 MERCEDES-BENZ E 350 2008 Sedan Yes Diesel 3.5 122874 km 6.0 Automatic Rear 04-May Left wheel Black 12
19229 39977395 50 <NA> TOYOTA Prius 2008 Hatchback No Hybrid 1.5 150000 km 4.0 Automatic Front 04-May Left wheel Silver 6
19230 45760891 470 645 TOYOTA Prius 2011 Hatchback Yes Hybrid 1.8 307325 km 4.0 Automatic Front 04-May Left wheel Silver 12
19231 45772306 5802 1055 MERCEDES-BENZ E 350 2013 Sedan Yes Diesel 3.5 107800 km 6.0 Automatic Rear 04-May Left wheel Grey 12
19232 45798355 8467 <NA> MERCEDES-BENZ CLK 200 1999 Coupe Yes CNG 2.0 Turbo 300000 km 4.0 Manual Rear 02-Mar Left wheel Silver 5
19233 45778856 15681 831 HYUNDAI Sonata 2011 Sedan Yes Petrol 2.4 161600 km 4.0 Tiptronic Front 04-May Left wheel Red 8
19234 45804997 26108 836 HYUNDAI Tucson 2010 Jeep Yes Diesel 2 116365 km 4.0 Automatic Front 04-May Left wheel Grey 4
19235 45793526 5331 1288 CHEVROLET Captiva 2007 Jeep Yes Diesel 2 51258 km 4.0 Automatic Front 04-May Left wheel Black 4
19236 45813273 470 753 HYUNDAI Sonata 2012 Sedan Yes Hybrid 2.4 186923 km 4.0 Automatic Front 04-May Left wheel White 12
In [6]:
df.shape
Out[6]:
(19237, 18)

Предлагаю исследовать в каких типах данных сохранились 18 признаков.

In [7]:
df.dtypes
Out[7]:
ID                    int64
Price                 int64
Levy                 object
Manufacturer         object
Model                object
Prod. year            int64
Category             object
Leather interior     object
Fuel type            object
Engine volume        object
Mileage              object
Cylinders           float64
Gear box type        object
Drive wheels         object
Doors                object
Wheel                object
Color                object
Airbags               int64
dtype: object

3) Операции над данными.¶

После осмотра данных необходимо выполнить определенные действия для дальнейшего удобства анализа.
Например - решение проблемы пустых значений, дубликатов и выбросов, удаление ненужных для текущей target признаков и приведение типов

3.1) Удаление признаков¶

Так как наша цель заключается в ранжировании признаков по их влиянию на ценообразование, я предлагаю удалить следующие признаки:

  • cylinders - так как основной акцент на ценообразование будет от типа двигателя, а не от количества цилиндров.
  • drive wheels - опять таки минимальный эффект на ценообразование. И кроме того тип привода завязан на моделе авто и его типа.
  • color - минимальное влияния на цену автомобиля.
  • airbags - минимальное влияние на цену автомобиля.
In [8]:
df = df.drop(["Cylinders", "Drive wheels", "Color", "Airbags"], axis=1)
df.head(5)
Out[8]:
ID Price Levy Manufacturer Model Prod. year Category Leather interior Fuel type Engine volume Mileage Gear box type Doors Wheel
0 45654403 13328 1399 LEXUS RX 450 2010 Jeep Yes Hybrid 3.5 186005 km Automatic 04-May Left wheel
1 44731507 16621 1018 CHEVROLET Equinox 2011 Jeep No Petrol 3 192000 km Tiptronic 04-May Left wheel
2 45774419 8467 <NA> HONDA FIT 2006 Hatchback No Petrol 1.3 200000 km Variator 04-May Right-hand drive
3 45769185 3607 862 FORD Escape 2011 Jeep Yes Hybrid 2.5 168966 km Automatic 04-May Left wheel
4 45809263 11726 446 HONDA FIT 2014 Hatchback Yes Petrol 1.3 91901 km Automatic 04-May Left wheel
3.2) Проверка категориальных признаков¶

Типы данных могут быть числовыми и категориальными. Поэтому следует рассмотреть их точнее и в последствии привести их к нужному типу.

In [9]:
df.select_dtypes(exclude="number").head()
Out[9]:
Levy Manufacturer Model Category Leather interior Fuel type Engine volume Mileage Gear box type Doors Wheel
0 1399 LEXUS RX 450 Jeep Yes Hybrid 3.5 186005 km Automatic 04-May Left wheel
1 1018 CHEVROLET Equinox Jeep No Petrol 3 192000 km Tiptronic 04-May Left wheel
2 <NA> HONDA FIT Hatchback No Petrol 1.3 200000 km Variator 04-May Right-hand drive
3 862 FORD Escape Jeep Yes Hybrid 2.5 168966 km Automatic 04-May Left wheel
4 446 HONDA FIT Hatchback Yes Petrol 1.3 91901 km Automatic 04-May Left wheel

Как мы видим Levy и Mileage скорее числовые признаки, нежели нечисловые. В Levy имеются пропуски, а в Mileage приписка km. Давайте это исправим.

Посмотрим на количество в Levy пустых значений.

In [10]:
df.isna().sum()
Out[10]:
ID                     0
Price                  0
Levy                5819
Manufacturer           0
Model                  0
Prod. year             0
Category               0
Leather interior       0
Fuel type              0
Engine volume          0
Mileage                0
Gear box type          0
Doors                  0
Wheel                  0
dtype: int64

Визуальное отображение пропусков:

In [11]:
plt.figure(figsize=(12, 6))
plt.imshow(df.isna(), aspect="auto", interpolation="nearest", cmap="gray")
plt.title("Visual display of passes\n", fontsize=16, fontweight="bold")
plt.xlabel("Column Number")
plt.ylabel("Sample Number")
Out[11]:
Text(0, 0.5, 'Sample Number')
No description has been provided for this image

Можно увидеть количество пропусков в признаке Levy - очень большое. И при этом признак, предполагается, одним из важнейших, так как содержит налог на автомобиль.
Следовательно, лучшим выходом будет выполнить заполнение по медиане.

In [12]:
df["Levy"] = df["Levy"].astype(
    "Int16"
)  # данный тип данных позволяет преобразовать признак с пропусками
df["Levy"] = df["Levy"].fillna(df["Levy"].median())
df.isna().sum()
Out[12]:
ID                  0
Price               0
Levy                0
Manufacturer        0
Model               0
Prod. year          0
Category            0
Leather interior    0
Fuel type           0
Engine volume       0
Mileage             0
Gear box type       0
Doors               0
Wheel               0
dtype: int64

Далее разберем признак Mileage, который необходимо привести в числовой тип, при этом убрав приписку km.

In [13]:
df["Mileage"] = df["Mileage"].str.replace(" km", "", regex=False)
df["Mileage"].head(10)
Out[13]:
0    186005
1    192000
2    200000
3    168966
4     91901
5    160931
6    258909
7    216118
8    398069
9    128500
Name: Mileage, dtype: object
In [14]:
df["Mileage"] = df["Mileage"].astype("int32")
df["Mileage"].head(10)
Out[14]:
0    186005
1    192000
2    200000
3    168966
4     91901
5    160931
6    258909
7    216118
8    398069
9    128500
Name: Mileage, dtype: int32

Еще раз выведем категориальные признаки, чтобы проверить не упустили ли мы что-то.

In [15]:
df.select_dtypes(exclude="number").head()
Out[15]:
Manufacturer Model Category Leather interior Fuel type Engine volume Gear box type Doors Wheel
0 LEXUS RX 450 Jeep Yes Hybrid 3.5 Automatic 04-May Left wheel
1 CHEVROLET Equinox Jeep No Petrol 3 Tiptronic 04-May Left wheel
2 HONDA FIT Hatchback No Petrol 1.3 Variator 04-May Right-hand drive
3 FORD Escape Jeep Yes Hybrid 2.5 Automatic 04-May Left wheel
4 HONDA FIT Hatchback Yes Petrol 1.3 Automatic 04-May Left wheel

Признак Engine volume оставляем нечисловым, так как помимо объема двигателя в признаке указывается - с турбонаддувом двигатель или нет

3.3) Проверка числовых признаков¶

Разберем числовые признаки dataset. Кроме того дополнительно проверим нахождения тут измененных признаков из пункта выше.

In [16]:
df.select_dtypes(include="number").head()
Out[16]:
ID Price Levy Prod. year Mileage
0 45654403 13328 1399 2010 186005
1 44731507 16621 1018 2011 192000
2 45774419 8467 781 2006 200000
3 45769185 3607 862 2011 168966
4 45809263 11726 446 2014 91901

Все хорошо, теперь можно рассмотреть количество уникальных значений в каждом из признаков. Это поможет точнее выбрать тип для приведения.

In [17]:
uniq_val = df.select_dtypes(include="number").nunique().sort_values()

fig = px.bar(
    x=uniq_val.index,
    y=uniq_val.values,
    labels={"x": "Features", "y": "Count"},
    # template="seaborn",
)

fig.update_layout(
    xaxis_tickangle=0,
    showlegend=False,
    title="Unique values in each feature\n",
)

fig.update_traces(texttemplate="%{y}", textposition="outside")

fig.show()
3.4) Проверка дубликатов¶

Так как мы хотим добиться уникальности записей, то необходимо провести над нашими данными проверку дубликатов по их ID.

In [18]:
df.shape
Out[18]:
(19237, 14)
In [19]:
duplic_rows_df = df[df.duplicated()]
duplic_rows_df.shape
Out[19]:
(313, 14)

Их оказалось не так много, учитывая весь объем датасета. Поэтому мы можем их смело удалить.

In [20]:
df = df.drop_duplicates()
df.shape
Out[20]:
(18924, 14)
3.5) Переименование признаков¶

Анализируя данные мы пришли к выводу, что необходимо переименовать некоторые признаки, так как они мало отражают содержащиеся в них сведения.
Все переименования будут указны также в файле NAMING.txt в директории docs.
Перед этим выведем все названия.

In [21]:
df.head()
Out[21]:
ID Price Levy Manufacturer Model Prod. year Category Leather interior Fuel type Engine volume Mileage Gear box type Doors Wheel
0 45654403 13328 1399 LEXUS RX 450 2010 Jeep Yes Hybrid 3.5 186005 Automatic 04-May Left wheel
1 44731507 16621 1018 CHEVROLET Equinox 2011 Jeep No Petrol 3 192000 Tiptronic 04-May Left wheel
2 45774419 8467 781 HONDA FIT 2006 Hatchback No Petrol 1.3 200000 Variator 04-May Right-hand drive
3 45769185 3607 862 FORD Escape 2011 Jeep Yes Hybrid 2.5 168966 Automatic 04-May Left wheel
4 45809263 11726 446 HONDA FIT 2014 Hatchback Yes Petrol 1.3 91901 Automatic 04-May Left wheel
In [22]:
df = df.rename(
    columns={
        "Levy": "Tax",
        "Prod. year": "Release_year",
        "Category": "Car_type",
        "Gear box type": "Transmission_type",
    }
)
df.head()
Out[22]:
ID Price Tax Manufacturer Model Release_year Car_type Leather interior Fuel type Engine volume Mileage Transmission_type Doors Wheel
0 45654403 13328 1399 LEXUS RX 450 2010 Jeep Yes Hybrid 3.5 186005 Automatic 04-May Left wheel
1 44731507 16621 1018 CHEVROLET Equinox 2011 Jeep No Petrol 3 192000 Tiptronic 04-May Left wheel
2 45774419 8467 781 HONDA FIT 2006 Hatchback No Petrol 1.3 200000 Variator 04-May Right-hand drive
3 45769185 3607 862 FORD Escape 2011 Jeep Yes Hybrid 2.5 168966 Automatic 04-May Left wheel
4 45809263 11726 446 HONDA FIT 2014 Hatchback Yes Petrol 1.3 91901 Automatic 04-May Left wheel
3.6) Обнаружение и обработка выбросов в числовых признаках¶

Важно предупредить момент выбросов и ошибочных огромных значений в числовых параметрах. Особенно перед приведением типов.
Такие выбросы будут негативно влиять на наш конечный вывод, что приведет к некорректному решению поставленной цели.
Вызовем для отображения снова числовые параметры.

In [23]:
df.select_dtypes(include="number").head()
Out[23]:
ID Price Tax Release_year Mileage
0 45654403 13328 1399 2010 186005
1 44731507 16621 1018 2011 192000
2 45774419 8467 781 2006 200000
3 45769185 3607 862 2011 168966
4 45809263 11726 446 2014 91901
In [24]:
df["Price"].nlargest(30)
Out[24]:
16983    26307500
8541       872946
1225       627220
5008       308906
9367       297930
14839      297930
7749       288521
10759      260296
5840       254024
15283      250574
7283       228935
2283       219527
7353       216391
1145       194438
13328      193184
4722       175622
2768       172486
2912       172486
6468       172486
9248       172486
7718       167781
13351      163077
4044       156805
11941      156805
15413      153669
10423      150533
17868      150533
7997       147397
18881      147397
13265      144261
Name: Price, dtype: int64
In [25]:
df["Tax"].nlargest(30)
Out[25]:
115      11714
18984    11714
18957    11706
17117     7536
3994      7063
2159      7058
5529      5908
5367      5877
17767     5681
2323      5679
14676     5679
8160      5666
2357      5603
17777     5603
3639      5332
19048     4860
17495     4741
9222      4736
1571      4508
8887      4283
14892     4057
11413     3989
13973     3965
16119     3910
10955     3894
18487     3811
14979     3743
6582      3739
16695     3699
18543     3571
Name: Tax, dtype: Int16
In [26]:
df["Release_year"].nlargest(30)
Out[26]:
956      2020
979      2020
1225     2020
1626     2020
2133     2020
3641     2020
4049     2020
4077     2020
4590     2020
5291     2020
5735     2020
6421     2020
7276     2020
7390     2020
7529     2020
9138     2020
9261     2020
9663     2020
10382    2020
10559    2020
10732    2020
11109    2020
11920    2020
12038    2020
12430    2020
12480    2020
12556    2020
12593    2020
12976    2020
13016    2020
Name: Release_year, dtype: int64
In [27]:
df["Mileage"].nlargest(30)
Out[27]:
2278     2147483647
6157     2147483647
11901    2147483647
12734    2147483647
15347    2147483647
15393    2147483647
19167    2147483647
17582    1777777778
7724     1234567899
9524     1111111111
19199    1111111111
5456      999999999
10667     999999999
12591     999999999
12904     999999999
16586     999999999
985       777777777
15364     222222222
18477     111111111
11144      58008888
1404       55556665
17206      40000000
11472      23000000
8695       20000000
9542       20000000
8824       18065445
18673      15000000
4823       12648846
1892       11111111
8486       11111111
Name: Mileage, dtype: int32

Как видно - проблемы есть. А конкретно в признаках Price, Tax, Mileage.
Это необходимо решать. Начнем с признака Price - приблизим до первых пяти максимальных значений.

In [28]:
df["Price"].nlargest(5)
Out[28]:
16983    26307500
8541       872946
1225       627220
5008       308906
9367       297930
Name: Price, dtype: int64

Давайте посмотрим какие автомобили имеют такой ценник.

In [29]:
df[df["Price"].isin([26307500, 872946, 627220])]
Out[29]:
ID Price Tax Manufacturer Model Release_year Car_type Leather interior Fuel type Engine volume Mileage Transmission_type Doors Wheel
1225 45795524 627220 781 MERCEDES-BENZ G 65 AMG 63AMG 2020 Jeep Yes Petrol 6.3 Turbo 0 Tiptronic 04-May Left wheel
8541 45761204 872946 2067 LAMBORGHINI Urus 2019 Universal Yes Petrol 4 2531 Tiptronic 04-May Left wheel
16983 45812886 26307500 781 OPEL Combo 1999 Goods wagon No Diesel 1.7 99999 Manual 02-Mar Left wheel

Мы нашли и проанализировали выбросы и первые два автомобиля - имеют адекватную цену. А вот третий - Opel Combo не может стоить такую сумму. Это необходимо исправить.

In [30]:
df["Price"] = df["Price"].mask(df["Price"] > 900000, df["Price"].median())
df["Price"].nlargest(5)
Out[30]:
8541     872946
1225     627220
5008     308906
9367     297930
14839    297930
Name: Price, dtype: int64

Что мы сделали?
Так как в признаке мы обнаружили и определили единичный выброс с суммой, которая невозможна для того типа автомобиля (цены указаны в долларах).
Поэтому было принято решение заменить этот выброс на медианное значение.
Ниже снова выведем максимальные значения признака Price:

In [31]:
df["Price"].nlargest(30)
Out[31]:
8541     872946
1225     627220
5008     308906
9367     297930
14839    297930
7749     288521
10759    260296
5840     254024
15283    250574
7283     228935
2283     219527
7353     216391
1145     194438
13328    193184
4722     175622
2768     172486
2912     172486
6468     172486
9248     172486
7718     167781
13351    163077
4044     156805
11941    156805
15413    153669
10423    150533
17868    150533
7997     147397
18881    147397
13265    144261
3006     141124
Name: Price, dtype: int64

Теперь проанализируем Tax похожим методом, как и Price.

In [32]:
df["Tax"].nlargest(10)
Out[32]:
115      11714
18984    11714
18957    11706
17117     7536
3994      7063
2159      7058
5529      5908
5367      5877
17767     5681
2323      5679
Name: Tax, dtype: Int16

Опять посмотрим на автомобили:

In [33]:
df[df["Tax"].isin([11714, 11706, 7536, 7063, 7058])]
Out[33]:
ID Price Tax Manufacturer Model Release_year Car_type Leather interior Fuel type Engine volume Mileage Transmission_type Doors Wheel
115 45534351 11917 11714 MERCEDES-BENZ E 500 AMG 2003 Sedan Yes Petrol 5 150000 Tiptronic 04-May Right-hand drive
2159 45804871 10349 7058 SUBARU Legacy 2005 Sedan Yes Petrol 3 147000 Tiptronic 04-May Right-hand drive
3994 45782188 13172 7063 TOYOTA Alphard 2003 Minivan Yes LPG 3 190000 Automatic 04-May Right-hand drive
17117 45781442 7213 7536 MITSUBISHI Pajero 2000 Jeep Yes CNG 3.2 210000 Automatic 04-May Right-hand drive
18957 44674964 14740 11706 MERCEDES-BENZ E 500 AVG 2005 Sedan Yes Petrol 5 56000 Tiptronic 04-May Right-hand drive
18984 45221191 11917 11714 MERCEDES-BENZ E 500 2003 Sedan Yes Petrol 5 150000 Tiptronic 04-May Right-hand drive

Просмотрев автомобили с такими налогами стало понятно - выбросов в признаке НЕТ!

Теперь проверим признак Mileage:

In [34]:
df["Mileage"].nlargest(10)
Out[34]:
2278     2147483647
6157     2147483647
11901    2147483647
12734    2147483647
15347    2147483647
15393    2147483647
19167    2147483647
17582    1777777778
7724     1234567899
9524     1111111111
Name: Mileage, dtype: int32

В этом признаке явно что-то аномальное (магические числа). Проверим с помощью межквартильного размаха(IQR).
Определение IQR - это статистическая мера, показывающая разброс средних 50% данных, отсекая крайние значения.

In [35]:
Q1_Mil = df["Mileage"].quantile(0.25)
Q3_Mil = df["Mileage"].quantile(0.75)
IQR_Mil = Q3_Mil - Q1_Mil
up_bound_Mil = Q3_Mil + 1.5 * IQR_Mil
down_bound_Mil = Q1_Mil - 1.5 * IQR_Mil

errors = df[df["Mileage"] > up_bound_Mil]
errors_type_2 = df[df["Mileage"] < down_bound_Mil]
len(errors)  # количество записей-выбросов выше верхней границы
Out[35]:
635
In [36]:
len(errors_type_2)  # количество записей-выбросов ниже нижней границы
Out[36]:
0

Делаем вывод, что 635 значений - у нас точно являются выбросами или некорректными значениями.
Так как у нас почти 19000 строчек, то принимаем решение по удалению этих выбросов.

In [37]:
df = df[
    df["Mileage"] <= up_bound_Mil
]  # оставляем в Mileage только те значения, которые попали до верхней границы
df["Mileage"].nlargest(10)
Out[37]:
1086     367053
7259     367000
14709    367000
4961     366869
8948     366869
9125     366869
10944    366869
15132    366869
16628    366869
9840     365810
Name: Mileage, dtype: int32
In [38]:
plt.figure(figsize=(12, 6))
sns.boxplot(x=df["Mileage"])
plt.title("Emissions in <Mileage> after delete\n", fontsize=16, fontweight="bold")
plt.show()
No description has been provided for this image

Вывод - мы задетектировали выбросы в числовых признаках и их убрали.

3.7) Приведение типов и сохранение в формате .parquet¶

Дальнейший анализ предполагает точный результат, поэтому я хотел бы использовать максимально обработанные и скорректированные данные. И в быстро работающем формате .parquet.
Выведем признаки и их типы:

In [39]:
df.dtypes
Out[39]:
ID                    int64
Price                 int64
Tax                   Int16
Manufacturer         object
Model                object
Release_year          int64
Car_type             object
Leather interior     object
Fuel type            object
Engine volume        object
Mileage               int32
Transmission_type    object
Doors                object
Wheel                object
dtype: object

Выполним приведение типов:

In [40]:
df["ID"] = df["ID"].astype("int32")
df["Price"] = df["Price"].astype("int32")
df["Manufacturer"] = df["Manufacturer"].astype("category")
df["Model"] = df["Model"].astype("category")
df["Tax"] = df["Tax"].astype("int32")
df["Release_year"] = df["Release_year"].astype("int16")
df["Car_type"] = df["Car_type"].astype("category")
df["Have a leather interior?"] = (
    df["Leather interior"].map({"No": 0, "Yes": 1}).astype("bool")
)  # добавляем к оригинальному признаку  булевый признак.

df["Car have a left wheel?"] = (
    df["Wheel"].map({"Right-hand drive": 0, "Left wheel": 1}).astype("bool")
)

df["Fuel type"] = df["Fuel type"].astype("category")
df["Transmission_type"] = df["Transmission_type"].astype("category")
df["Doors"] = df["Doors"].astype("category")
df[["Leather interior", "Wheel"]] = df[
    ["Have a leather interior?", "Car have a left wheel?"]
]  # заменяем оригинал на булевый.
df = df.drop(
    columns=["Have a leather interior?", "Car have a left wheel?"]
)  # удаляем дополнительные признаки.

df.rename(
    columns={"Leather interior": "Have a leather interior?"}, inplace=True
)  # переименовываем.
df.rename(columns={"Wheel": "Car have a left wheel?"}, inplace=True)

df.dtypes
Out[40]:
ID                             int32
Price                          int32
Tax                            int32
Manufacturer                category
Model                       category
Release_year                   int16
Car_type                    category
Have a leather interior?        bool
Fuel type                   category
Engine volume                 object
Mileage                        int32
Transmission_type           category
Doors                       category
Car have a left wheel?          bool
dtype: object

Мы привели типы и теперь сохраним наш новый рабочий dataset в формате .parquet

In [41]:
fi_path = os.path.join(data_dir, "data_car.parquet")  # путь до файла dataset
df.to_parquet(fi_path)  # сохраняем dataset в .parquet
df_par = pd.read_parquet(fi_path)
df_par.head()
Out[41]:
ID Price Tax Manufacturer Model Release_year Car_type Have a leather interior? Fuel type Engine volume Mileage Transmission_type Doors Car have a left wheel?
0 45654403 13328 1399 LEXUS RX 450 2010 Jeep True Hybrid 3.5 186005 Automatic 04-May True
1 44731507 16621 1018 CHEVROLET Equinox 2011 Jeep False Petrol 3 192000 Tiptronic 04-May True
2 45774419 8467 781 HONDA FIT 2006 Hatchback False Petrol 1.3 200000 Variator 04-May False
3 45769185 3607 862 FORD Escape 2011 Jeep True Hybrid 2.5 168966 Automatic 04-May True
4 45809263 11726 446 HONDA FIT 2014 Hatchback True Petrol 1.3 91901 Automatic 04-May True

4) Проверка по метрикам¶

Решим проходит ли наш dataset по метрикам - Completeness и Uniqueness.

In [42]:
completeness = df_par.count() / len(df_par)
uniqueness = df_par["ID"].nunique() / len(
    df_par["ID"]
)  # так как этот параметр напрямую отвечает за уникальность значений в признаках.
print(completeness)
print(f"{'-'*60}")
print(f"ID = {uniqueness}")
ID                          1.0
Price                       1.0
Tax                         1.0
Manufacturer                1.0
Model                       1.0
Release_year                1.0
Car_type                    1.0
Have a leather interior?    1.0
Fuel type                   1.0
Engine volume               1.0
Mileage                     1.0
Transmission_type           1.0
Doors                       1.0
Car have a left wheel?      1.0
dtype: float64
------------------------------------------------------------
ID = 1.0

5) Валидация данных¶

Проведем валидацию корректности данных с помощью assert.
Если проверки будут успешными - то переходим к графикам и диаграммам.

In [43]:
assert (df_par["Price"].dtype == "int32") and ((df_par["Price"] >= 0).all()), print(
    "ERROR in Price"
)
assert (df_par["Tax"].dtype == "int32") and ((df_par["Tax"] >= 0).all()), print(
    "ERROR in Tax"
)

assert (
    (df_par["Release_year"].dtype == "int16")
    and ((df_par["Release_year"] >= 1900).all())
    and ((df_par["Release_year"] <= 2100).all())
), print("ERROR in Release_year")

assert (df_par["Have a leather interior?"].dtype == "bool") and (
    (df_par["Have a leather interior?"].isin([True, False]).all())
), print("ERROR in Have a leather interior?")

assert (df_par["Mileage"].dtype == "int32") and ((df_par["Mileage"] >= 0).all()), print(
    "ERROR in Mileage"
)

assert (df_par["Car have a left wheel?"].dtype == "bool") and (
    (df_par["Car have a left wheel?"].isin([True, False]).all())
), print("ERROR in Car have a left wheel?")

Валидация числовых и булевых признаков - прошла успешно!


6) Анализы данных для решения задачи¶

6.1) Анализ распределения целевой переменной - Price¶

Повторно озвучиваем цель - провести ранжирование признаков по влиянию на ценообразования автомобиля.
Следовательно основной целевой переменной мы берем признак Price. Выведем гистограмму.

In [44]:
fig = px.histogram(
    df_par,
    x="Price",
    nbins=50,
    # template="seaborn",
)

fig.update_layout(
    xaxis_tickangle=0,
    showlegend=False,
    title="Price distribution",
)

fig.show()

Как мы обсуждали выше - цена выше 600000 долларов у нескольких автомобилей - это нормальная цена.

6.2) Построение корреляционной карты и точечных графиков для коррелирующих признаков¶

Для определения корреляций между признаками воспользуемся методами heatmap и scatterplot.

In [45]:
num_df_par = df_par.select_dtypes(include="number").columns

plt.figure(figsize=(10, 8))
sns.heatmap(df_par[num_df_par].corr(), annot=True, cmap="OrRd")
plt.title("The correlation matrix\n", fontsize=16, fontweight="bold")
plt.show()

top_corr_ft = (
    df_par[num_df_par].corr()["Price"].abs().sort_values(ascending=False).index[1:4]
)
for features in top_corr_ft:
    sns.scatterplot(data=df_par, x=features, y="Price")
    plt.title(f"<Price> vs <{features}>\n", fontsize=16, fontweight="bold")
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Предлагаю рассмотреть распределения признаков Release_year, Mileage, Tax:

In [46]:
num_cols = ["Release_year", "Mileage", "Tax"]  # числовые признаки

fig, axes = plt.subplots(2, 2, figsize=(12, 6))
fig.suptitle("Distribution of numerical features\n", fontsize=16, fontweight="bold")

axes_flat = axes.flat  # преобразование массива 2х2 в массив 1:4 для перебора элементов

for i, col in enumerate(num_cols):
    sns.histplot(data=df_par, x=col, ax=axes_flat[i], bins=50, kde=True)
    axes_flat[i].set_title(f"Distribution <{col}>\n")
    axes_flat[i].tick_params(axis="x", rotation=0)

for i in range(len(num_cols), len(axes_flat)):
    axes_flat[i].set_visible(False)

plt.tight_layout()
plt.show()
No description has been provided for this image

Промежуточный вывод - мы получили наши точечные графики корреляции признаков с основным признаком Price.

Сразу заметно, что эти графики представляют собой своеобразное "облако из точек" и в некоторых ситуациях, например с признаками Mileage и Pelease_year, можно заметить что-то похожее на, в первом случае нисходящую прямую линию, а во втором случае восходящую прямую линию.
Что может говорить нам следующее:

  • С увеличением пробега - цена автомобиля падает.
  • Чем новее выпущен автомобиль - тем выше его цена.

Но не стоит забыть, что в основном это "облака из точек", т.е интерпретируя это показывает на достаточно слабую зависимость.

Также гипотеза о слабой зависимости Price от числовых параметров подтверждается коррелляционной матрицей. Так как максимальное число в ней, из положительных естественно, это 0,27 - что, исходя из определения коэффициента корреляции Пирсона, говорит нам о слабой положительной корреляции.

Кроме того из распределений сделаем следующие выводы:

  1. В основном в нем представлены автомобили выпуска 2000-2018 годов.
  2. Среднем значение пробега - примерно от 100000 до 150000 км.
  3. Средний налог на авто - 500 долларов

А эти данные указывают на то, что датасет создавался на основе реальных величин и реальных показателей в автомобильной промышленности.

Теперь проведем проверку категориальных признаков:
!!!Примечание!!! - выведем все признаки кроме Model, так как последний из-за количества уникальных наименований - не отображается.

In [47]:
plt.rcParams["font.family"] = "DejaVu Sans"
plt.rcParams["font.sans-serif"] = ["DejaVu Sans", "Arial Unicode MS", "Liberation Sans"]

cat_ft = df_par.select_dtypes(exclude="number").columns

feature_manufacturer = cat_ft[0]
plt.figure(figsize=(25, 10))

unique_categories = df_par[
    feature_manufacturer
].unique()  # получаем уникальные значения в признаке

for i, category in enumerate(unique_categories):
    category_data = df_par[
        df_par[feature_manufacturer] == category
    ]  # содержит строки соответствующие уникальному значению

    if len(category_data) > 0:
        min_price = category_data[
            "Price"
        ].min()  # нормализует цены из долларов в диапазон от 0 до 1
        max_price = category_data["Price"].max()

        if max_price > min_price:
            normalized_prices = (category_data["Price"] - min_price) / (
                max_price - min_price
            )
        else:
            normalized_prices = pd.Series([0.5] * len(category_data))

        color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(
            int
        )  # в соответствии с нормализацией красит каждое уникальное значение
        colors_for_category = [translate_into_hex[idx] for idx in color_indices]

        x_pos = i + np.random.normal(
            0, 0.001, size=len(category_data)
        )  # специально заданные разброс точек, для лучшего их отображения
        # x_pos = [i] * len(category_data)
        plt.scatter(
            x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
        )

plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_manufacturer}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [48]:
feature_car_type = cat_ft[2]
plt.figure(figsize=(25, 10))

unique_categories = df_par[feature_car_type].unique()

for i, category in enumerate(unique_categories):
    category_data = df_par[df_par[feature_car_type] == category]

    if len(category_data) > 0:
        min_price = category_data["Price"].min()
        max_price = category_data["Price"].max()

        if max_price > min_price:
            normalized_prices = (category_data["Price"] - min_price) / (
                max_price - min_price
            )
        else:
            normalized_prices = pd.Series([0.5] * len(category_data))

        color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
        colors_for_category = [translate_into_hex[idx] for idx in color_indices]

        x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
        plt.scatter(
            x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
        )

plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_car_type}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [49]:
feature_leather = cat_ft[3]
plt.figure(figsize=(25, 10))

unique_categories = df_par[feature_leather].unique()

for i, category in enumerate(unique_categories):
    category_data = df_par[df_par[feature_leather] == category]

    if len(category_data) > 0:
        min_price = category_data["Price"].min()
        max_price = category_data["Price"].max()

        if max_price > min_price:
            normalized_prices = (category_data["Price"] - min_price) / (
                max_price - min_price
            )
        else:
            normalized_prices = pd.Series([0.5] * len(category_data))

        color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
        colors_for_category = [translate_into_hex[idx] for idx in color_indices]

        x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
        # x_pos = [i] * len(category_data)
        plt.scatter(
            x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
        )

plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_leather}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [50]:
feature_fuel_type = cat_ft[4]
plt.figure(figsize=(25, 10))

unique_categories = df_par[feature_fuel_type].unique()

for i, category in enumerate(unique_categories):
    category_data = df_par[df_par[feature_fuel_type] == category]

    if len(category_data) > 0:
        min_price = category_data["Price"].min()
        max_price = category_data["Price"].max()

        if max_price > min_price:
            normalized_prices = (category_data["Price"] - min_price) / (
                max_price - min_price
            )
        else:
            normalized_prices = pd.Series([0.5] * len(category_data))

        color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
        colors_for_category = [translate_into_hex[idx] for idx in color_indices]

        x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
        # x_pos = [i] * len(category_data)
        plt.scatter(
            x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
        )

plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_fuel_type}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [51]:
feature_engine_vol = cat_ft[5]
plt.figure(figsize=(25, 10))

unique_categories = df_par[feature_engine_vol].unique()

for i, category in enumerate(unique_categories):
    category_data = df_par[df_par[feature_engine_vol] == category]

    if len(category_data) > 0:
        min_price = category_data["Price"].min()
        max_price = category_data["Price"].max()

        if max_price > min_price:
            normalized_prices = (category_data["Price"] - min_price) / (
                max_price - min_price
            )
        else:
            normalized_prices = pd.Series([0.5] * len(category_data))

        color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
        colors_for_category = [translate_into_hex[idx] for idx in color_indices]

        x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
        # x_pos = [i] * len(category_data)
        plt.scatter(
            x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
        )

plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_engine_vol}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [52]:
feature_transmission = cat_ft[6]
plt.figure(figsize=(25, 10))

unique_categories = df_par[feature_transmission].unique()

for i, category in enumerate(unique_categories):
    category_data = df_par[df_par[feature_transmission] == category]

    if len(category_data) > 0:
        min_price = category_data["Price"].min()
        max_price = category_data["Price"].max()

        if max_price > min_price:
            normalized_prices = (category_data["Price"] - min_price) / (
                max_price - min_price
            )
        else:
            normalized_prices = pd.Series([0.5] * len(category_data))

        color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
        colors_for_category = [translate_into_hex[idx] for idx in color_indices]

        x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
        # x_pos = [i] * len(category_data)
        plt.scatter(
            x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
        )

plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_transmission}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [53]:
feature_doors = cat_ft[7]
plt.figure(figsize=(25, 10))

unique_categories = df_par[feature_doors].unique()

for i, category in enumerate(unique_categories):
    category_data = df_par[df_par[feature_doors] == category]

    if len(category_data) > 0:
        min_price = category_data["Price"].min()
        max_price = category_data["Price"].max()

        if max_price > min_price:
            normalized_prices = (category_data["Price"] - min_price) / (
                max_price - min_price
            )
        else:
            normalized_prices = pd.Series([0.5] * len(category_data))

        color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
        colors_for_category = [translate_into_hex[idx] for idx in color_indices]

        x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
        # x_pos = [i] * len(category_data)
        plt.scatter(
            x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
        )

plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_doors}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [54]:
feature_wheel = cat_ft[8]
plt.figure(figsize=(25, 10))

unique_categories = df_par[feature_wheel].unique()

for i, category in enumerate(unique_categories):
    category_data = df_par[df_par[feature_wheel] == category]

    if len(category_data) > 0:
        min_price = category_data["Price"].min()
        max_price = category_data["Price"].max()

        if max_price > min_price:
            normalized_prices = (category_data["Price"] - min_price) / (
                max_price - min_price
            )
        else:
            normalized_prices = pd.Series([0.5] * len(category_data))

        color_indices = (normalized_prices * (len(translate_into_hex) - 1)).astype(int)
        colors_for_category = [translate_into_hex[idx] for idx in color_indices]

        x_pos = i + np.random.normal(0, 0.001, size=len(category_data))
        # x_pos = [i] * len(category_data)
        plt.scatter(
            x_pos, category_data["Price"], c=colors_for_category, alpha=0.7, s=30
        )

plt.xticks(range(len(unique_categories)), unique_categories, rotation=90)
plt.title(f"<Price> vs <{feature_wheel}>", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
No description has been provided for this image

К сожалению в связи с огромным количеством уникальных значений в признаке Model взять информацию с графика не представляется возможным. Но какие выводы можно сделать по оставшимся:

Промежуточный вывод - благодаря анализу с помощью scatterplot стало известно, что большинство автомобилей в dataset - находятся в достаточно дешевом сегменте. Есть редкие исключения в виде дорогих, но там большую роль играет бренд.
Также, как я писал и ранее - облака точек могут соответствовать тому, что у нас имеется минимальная корреляция признаков на признак Price. Это надо проверить дальше.

Вернем шрифт на первоначальный.

In [55]:
plt.rcParams["font.family"] = "sans-serif"
plt.rcParams["font.sans-serif"] = ["Arial", "DejaVu Sans", "Liberation Sans"]

Далее, поиск в интернете и подсказки от AI, направили меня на поиск модели для создания ранжирования признаков по влиянию на цену.
Так была найдена библиотека scikit-learn откуда взята модель RandomForestRegressor. Код ниже это процесс работы данной модели.

In [56]:
from sklearn.ensemble import RandomForestRegressor


ft = [
    col for col in df_par.columns if col not in ["Price", "ID"]
]  # исключаем ID и Price как признаки из анализа

X = df_par[ft].copy()  # на ось X ставятся наши признаки
y = df_par["Price"]

X_encoded = X.copy()
for col in X_encoded.select_dtypes(exclude="number").columns:
    X_encoded[col] = (
        X_encoded[col].astype("category").cat.codes
    )  # перевод категориальных признаков в числа

model = RandomForestRegressor(
    random_state=10, n_estimators=100
)  # cама модель, где n_estimators - количество деревьев решений(каждое анализирует по-своему), random_state - только для воспроизводимости результатов (может быть любым)
model.fit(X_encoded, y)

feature_importance = pd.DataFrame(
    {"feature": ft, "importance": model.feature_importances_}
).sort_values(
    "importance", ascending=False
)  # сортировка по важности

plt.figure(figsize=(12, 6))
sns.barplot(
    data=feature_importance.head(12),
    x="importance",
    y="feature",
    hue="feature",
    palette="OrRd_r",
    legend=False,
)
plt.title("Top 12 significant features for car prices", fontsize=16, fontweight="bold")
plt.xlabel(
    "The importance of the feature",
)
plt.ylabel("")
plt.tight_layout()
plt.show()

# Выводим таблицу с результатами
print("Ranking of features by importance:")
print("=" * 50)
for i, row in feature_importance.iterrows():
    print(f"{i+1:2d}. {row['feature']:25} | importance: {row['importance']:.4f}")
No description has been provided for this image
Ranking of features by importance:
==================================================
 8. Engine volume             | importance: 0.2355
 4. Release_year              | importance: 0.2153
 9. Mileage                   | importance: 0.1403
 3. Model                     | importance: 0.0956
 1. Tax                       | importance: 0.0767
10. Transmission_type         | importance: 0.0659
 7. Fuel type                 | importance: 0.0588
 5. Car_type                  | importance: 0.0471
 2. Manufacturer              | importance: 0.0436
 6. Have a leather interior?  | importance: 0.0125
11. Doors                     | importance: 0.0067
12. Car have a left wheel?    | importance: 0.0020

Рассмотрим также корреляцию основных числовых признаков с помощью 3D scatterplot. Как мы выяснили основными будут - Release year и Mileage.

In [57]:
if len(top_corr_ft) >= 2:
    fig = px.scatter_3d(
        df_par,
        x=top_corr_ft[0],
        y=top_corr_ft[1],
        z="Price",
        color="Price",
        title=f"3D scatter: <{top_corr_ft[0]}> vs <{top_corr_ft[1]}> vs <Price>",
        # template="seaborn",
        color_continuous_scale="OrRd_r",
    )
    fig.show()

Промежуточный вывод - чем новее автомобиль и чем меньше у него пробег, тем формируемая цена - выше. Как и требовалось доказать. Но, на графике также видны 2 точки, выбивающиеся из картины по причине сильного воздействия на цену другого признака - Model


7) Итоги¶

Это было сложно и местами непонятно, но мы добрались до конца анализа данных!

Итог - задача была решена. Из графика barplot мы видим, что наибольшее влияние на стоимость автомобиля оказывает два признака Engine_volume и Release_year. Далее все идет по убыванию.
Какой вывод сделал лично я? - есть ощущение, что данные были синтетическими, так как в них на предыдущих этапах были странные выбросы - скорее похожие на ошибки, которые создавал человек. Кроме того присутствовали магические числа в некоторых признаках, а в признаке Tax и только там были пустые значения.
Все это наводит на мысли об искуственности данных

Но при этом же топ-3 признака в итоговой модели показывают на близкие к реальности факты:

  • Чем старше автомобиль, тем он дешевле (исключая старые ретро-автомобили, которые не укладываются в такую модель).
  • Чем больше у авто объем двигателя (а соответственно и мощность), тем он дороже. Причем это фактор №1 исходя из ранжирования.
  • Чем меньше у автомобиля километраж, тем он дороже.

Что меня удивило:

  • Удивил факт того, что тип автомобиля, производитель и модель - являются не самыми главными факторами в цене автомобиля. Но стоит учитывать, что модель и производитель решают среднюю цену, ибо Lamborgini и UAZ их медиана цены находится в разных местах.
  • Также удивило, что тип руля оказывает наименьшее влияния на цену. Хотя при покупке машины из-за рубежа, особенно с Японии, если у машины правый руль, то и стоит она дешевле.

ВЫВОД - в большинстве своем анализ показал реальную картину формирования стоимости автомобиля, но есть подозрения в том, что взятый dataset с kaggle имеет искусственное происхождение.